﻿/*
Copyright © 2011 François Karman

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

See <license.txt> for the complete license of the software.
*/

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.IsolatedStorage;
using Simple.Storage.Serializer;

namespace Simple.Storage.Api
{
    /// <summary>
    /// Implements the <see cref="IDataSource"/> contract.
    /// </summary>
    internal class DataSource : IDataSource
    {
        /// <summary>
        /// The associated data source definition.
        /// </summary>
        private readonly IDataSourceDefinition definition;

        /// <summary>
        /// The associated class definitions.
        /// </summary>
        private readonly IDictionary<Type, IClassDefinition> classes;

        /// <summary>
        /// The read-only wrapper for <see cref="DataSource.classes"/>.
        /// </summary>
        private readonly IDictionary<Type, IClassDefinition> classesRO;

        /// <summary>
        /// The associated index definitions.
        /// </summary>
        private readonly IDictionary<Type, IDictionary<string, IIndexDefinition>> indexes;

        /// <summary>
        /// The read-only wrapper for <see cref="DataSource.indexes"/>.
        /// </summary>
        private readonly IDictionary<Type, IDictionary<string, IIndexDefinition>> indexesRO;

        /// <summary>
        /// The serializer associated with the class definitions.
        /// </summary>
        private readonly IDictionary<Type, ISerializer> classSerializers;

        /// <summary>
        /// The serializer associated with the property definitions.
        /// </summary>
        private readonly GroupedSerializer propertySerializers;

        /// <summary>
        /// The index managers created for each persisted class.
        /// </summary>
        private readonly IDictionary<Type, IndexManager> indexManagers;

        /// <summary>
        /// The unique name of the data source.
        /// </summary>
        private readonly string name;

        /// <summary>
        /// The cache used when saving an object.
        /// </summary>
        private readonly SavingCache savingCache;

        /// <summary>
        /// The cache used when loading an object.
        /// </summary>
        private readonly LoadingCache loadingCache;

        /// <summary>
        /// Initializes a new instance of the DataSource class.
        /// </summary>
        /// <param name="definition">The associated data source definition.</param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="definition"/> parameter is <c>null</c>.
        /// </exception>
        public DataSource(IDataSourceDefinition definition)
        {
            if (definition == null)
            {
                throw new ArgumentNullException("definition");
            }

            this.classes = new Dictionary<Type, IClassDefinition>();
            this.classesRO = new ReadOnlyDictionary<Type, IClassDefinition>(this.classes);
            this.indexes = new Dictionary<Type, IDictionary<string, IIndexDefinition>>();
            this.indexesRO = new ReadOnlyDictionary<Type, IDictionary<string, IIndexDefinition>>(this.indexes);
            this.classSerializers = new Dictionary<Type, ISerializer>();
            this.propertySerializers = new GroupedSerializer();
            this.indexManagers = new Dictionary<Type, IndexManager>();
            this.savingCache = new SavingCache();
            this.loadingCache = new LoadingCache();

            this.definition = definition;
            this.name = definition.Name;
        }

        /// <summary>
        /// Occurs before saving an object.
        /// </summary>
        public event EventHandler<InstanceEventArgs> Saving;

        /// <summary>
        /// Occurs before loading an object.
        /// </summary>
        public event EventHandler<KeyEventArgs> Loading;

        /// <summary>
        /// Occurs before deleting an object.
        /// </summary>
        public event EventHandler<KeyEventArgs> Deleting;

        /// <summary>
        /// Occurs after saving an object.
        /// </summary>
        public event EventHandler<InstanceEventArgs> Saved;

        /// <summary>
        /// Occurs after loading an object.
        /// </summary>
        public event EventHandler<InstanceEventArgs> Loaded;

        /// <summary>
        /// Occurs after deleting an object.
        /// </summary>
        public event EventHandler<KeyEventArgs> Deleted;

        /// <summary>
        /// Gets the unique name of the data source.
        /// </summary>
        public string Name
        {
            get { return this.name; }
        }

        /// <summary>
        /// Gets the associated class definitions.
        /// </summary>
        public IDictionary<Type, IClassDefinition> Classes
        {
            get { return this.classesRO; }
        }

        /// <summary>
        /// Gets the associated index definitions.
        /// </summary>
        public IDictionary<Type, IDictionary<string, IIndexDefinition>> Indexes
        {
            get { return this.indexesRO; }
        }

        /// <summary>
        /// Creates a new query to extract values for a specified persisted class.
        /// </summary>
        /// <typeparam name="T">The type of the persisted class.</typeparam>
        /// <returns>The query object.</returns>
        /// <exception cref="UnknownClassException">
        /// <typeparamref name="T"/> is not a persisted class.
        /// </exception>
        [SuppressMessage(
            "Microsoft.Design",
            "CA1004:GenericMethodsShouldProvideTypeParameter",
            Justification = "Implements IDataSource")]
        public IEnumerable<IDescriptor<T>> Query<T>()
        {
            if (!this.indexManagers.ContainsKey(typeof(T)))
            {
                string message = string.Format(Messages.Culture, Messages.DataSourceUnknownClass, typeof(T).FullName);
                throw new UnknownClassException(message);
            }

            return this.indexManagers[typeof(T)].ToQuery<T>();
        }

        /// <summary>
        /// Loads a specified persisted instance.
        /// </summary>
        /// <typeparam name="T">The type of the retrieved instance.</typeparam>
        /// <param name="key">The key of the retrieved instance.</param>
        /// <returns>The retrieved instance.</returns>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="key"/> parameter is <c>null</c>.
        /// </exception>
        /// <exception cref="UnknownClassException">
        /// <typeparamref name="T"/> is not a persisted class.
        /// </exception>
        /// <exception cref="WrongKeyTypeException">
        /// The <paramref name="key"/> parameter is not of the expected type.
        /// </exception>
        /// <exception cref="KeyNotFoundException">
        /// There is no element associated with the specified <paramref name="key"/>.
        /// </exception>
        [SuppressMessage(
            "Microsoft.Design",
            "CA1004:GenericMethodsShouldProvideTypeParameter",
            Justification = "Implements IDataSource")]
        public T Load<T>(object key)
        {
            this.loadingCache.Clear();
            T result = (T)this.Load(typeof(T), key);
            if (result == null)
            {
                // TODO: Message
                throw new KeyNotFoundException();
            }

            return result;
        }

        /// <summary>
        /// Persists a specified instance.
        /// </summary>
        /// <typeparam name="T">The type of the saved instance.</typeparam>
        /// <param name="value">The value to persist.</param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="value"/> parameter is <c>null</c>.
        /// </exception>
        /// <exception cref="UnknownClassException">
        /// <typeparamref name="T"/> is not a persisted class.
        /// </exception>
        public void Save<T>(T value)
        {
            this.savingCache.Clear();
            this.Save(typeof(T), value);
        }

        /// <summary>
        /// Deletes a specified instance.
        /// </summary>
        /// <typeparam name="T">The type of the deleted instance.</typeparam>
        /// <param name="key">The key of the deleted instance.</param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="key"/> parameter is <c>null</c>.
        /// </exception>
        /// <exception cref="UnknownClassException">
        /// <typeparamref name="T"/> is not a persisted class.
        /// </exception>
        /// <exception cref="WrongKeyTypeException">
        /// The <paramref name="key"/> parameter is not of the expected type.
        /// </exception>
        /// <exception cref="KeyNotFoundException">
        /// There is no element associated with the specified <paramref name="key"/>.
        /// </exception>
        [SuppressMessage(
            "Microsoft.Design",
            "CA1004:GenericMethodsShouldProvideTypeParameter",
            Justification = "Implements IDataSource")]
        public void Delete<T>(object key)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }
            else if (!this.classes.ContainsKey(typeof(T)))
            {
                string message = string.Format(Messages.Culture, Messages.DataSourceUnknownClass, typeof(T).FullName);
                throw new UnknownClassException(message);
            }

            IClassDefinition definition = this.classes[typeof(T)];
            if (!definition.KeyType.IsInstanceOfType(key))
            {
                string message = string.Format(
                    Messages.Culture,
                    Messages.DataSourceWrongKeyType,
                    typeof(T).FullName,
                    definition.KeyType.FullName,
                    key.GetType().FullName,
                    key);
                throw new WrongKeyTypeException(message);
            }

            // Raise the deleting event
            if (this.Deleting != null)
            {
                this.Deleting(this, new KeyEventArgs(typeof(T), key));
            }

            using (IsolatedStorageFile store = StorageFactory.Create())
            {
                string path = this.DefineInstancePath(definition, key);
                if (store.FileExists(path))
                {
                    store.DeleteFile(path);

                    // Clear indexes
                    this.indexManagers[typeof(T)].OnDelete(key);

                    // Raise the deleted event
                    if (this.Deleted != null)
                    {
                        this.Deleted(this, new KeyEventArgs(typeof(T), key));
                    }
                }
                else
                {
                    // TODO: Add a message
                    throw new KeyNotFoundException();
                }
            }
        }

        /// <summary>
        /// Removes and cleans everything associated with the data source.
        /// As expected, this action is irreversible.
        /// </summary>
        public void RemoveEverything()
        {
            // TODO: clean indexes and others internal status
            IsolatedStorageHelper.DeleteDirectory(this.Name);
        }

        /// <summary>
        /// Initializes the internal values of the data source
        /// and validates its definition.
        /// </summary>
        /// <param name="initializer">
        /// The initializer provided by the engine.
        /// </param>
        public void Initialize(IInitializer initializer)
        {
            if (initializer == null)
            {
                throw new ArgumentNullException("initializer");
            }

            // Validate the configuration
            this.classes.Clear();
            foreach (IClassDefinition classDefinition in this.definition.Classes)
            {
                if (classDefinition == null)
                {
                    throw new DefinitionException(Messages.DataSourceNullClass);
                }
                else if (classDefinition.Type == null)
                {
                    throw new DefinitionException(Messages.DataSourceEmptyClassType);
                }
                else if (classDefinition.KeyType == null)
                {
                    string message = string.Format(Messages.Culture, Messages.DataSourceEmptyKeyType, classDefinition.Type);
                    throw new DefinitionException(message);
                }
                else if (this.classes.ContainsKey(classDefinition.Type))
                {
                    string message = string.Format(Messages.Culture, Messages.DataSourceDoubleClassDefinition, classDefinition.Type);
                    throw new DefinitionException(message);
                }

                this.classes.Add(classDefinition.Type, classDefinition);
            }

            this.indexes.Clear();
            foreach (IClassDefinition classDefinition in this.definition.Classes)
            {
                this.indexes.Add(classDefinition.Type, new Dictionary<string, IIndexDefinition>());
            }

            foreach (IIndexDefinition indexDefinition in this.definition.Indexes)
            {
                if (indexDefinition == null)
                {
                    throw new DefinitionException(Messages.DataSourceNullIndex);
                }
                else if (string.IsNullOrEmpty(indexDefinition.Name))
                {
                    throw new DefinitionException(Messages.DataSourceEmptyIndexName);
                }
                else if (indexDefinition.Type == null)
                {
                    string message = string.Format(Messages.Culture, Messages.DataSourceEmptyIndexType, indexDefinition.Name);
                    throw new DefinitionException(message);
                }
                else if (indexDefinition.IndexType == null)
                {
                    string message = string.Format(Messages.Culture, Messages.DataSourceEmptyIndexValueType, indexDefinition.Name);
                    throw new DefinitionException(message);
                }
                else if (!this.classes.ContainsKey(indexDefinition.Type))
                {
                    string message = string.Format(Messages.Culture, Messages.DataSourceWrongIndexType, indexDefinition.Name, indexDefinition.Type);
                    throw new DefinitionException(message);
                }

                if (this.indexes[indexDefinition.Type].ContainsKey(indexDefinition.Name))
                {
                    string message = string.Format(Messages.Culture, Messages.DataSourceDoubleIndexDefinition, indexDefinition.Name, indexDefinition.Type);
                    throw new DefinitionException(message);
                }

                this.indexes[indexDefinition.Type].Add(indexDefinition.Name, indexDefinition);
            }

            // Create the link serializers
            this.propertySerializers.Clear();
            foreach (IClassDefinition classDefinition in this.definition.Classes)
            {
                ISerializer linkSerializer = initializer.CreateLinkSerializer(this, classDefinition, this.propertySerializers);
                this.propertySerializers.Add(linkSerializer);
            }

            // Retrieve the default serializers
            initializer.CreateSerializers(this, this.propertySerializers);

            // Create the class serializers
            foreach (IClassDefinition classDefinition in this.definition.Classes)
            {
                ISerializer classSerializer = initializer.CreateClassSerializer(this, classDefinition, this.propertySerializers);
                this.classSerializers.Add(classDefinition.Type, classSerializer);
            }

            // Create the index managers
            foreach (IClassDefinition classDefinition in this.definition.Classes)
            {
                // make indexes unmodifiable
                this.indexes[classDefinition.Type]
                    = new ReadOnlyDictionary<string, IIndexDefinition>(this.indexes[classDefinition.Type]);

                // create index manager
                this.indexManagers.Add(classDefinition.Type, new IndexManager(this, classDefinition.Type));
            }

            // Load existing index data
            this.LoadIndexes();
        }

        /// <summary>
        /// Ensures that the indexes ave properly saved before releasing the instance.
        /// </summary>
        public void Dispose()
        {
            this.SaveIndexes();
        }

        /// <summary>
        /// Implements the loading logic.
        /// </summary>
        /// <param name="type">The type of the loaded value.</param>
        /// <param name="key">The key of the loaded value.</param>
        /// <returns>The loaded value.</returns>
        /// <seealso cref="DataSource.Load{T}(object)"/>
        internal object Load(Type type, object key)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }
            else if (key == null)
            {
                throw new ArgumentNullException("key");
            }
            else if (!this.classes.ContainsKey(type))
            {
                string message = string.Format(Messages.Culture, Messages.DataSourceUnknownClass, type.FullName);
                throw new UnknownClassException(message);
            }

            IClassDefinition definition = this.classes[type];
            if (!definition.KeyType.IsInstanceOfType(key))
            {
                string message = string.Format(
                    Messages.Culture,
                    Messages.DataSourceWrongKeyType,
                    type.FullName,
                    definition.KeyType.FullName,
                    key.GetType().FullName,
                    key);
                throw new WrongKeyTypeException(message);
            }

            if (this.loadingCache.IsLoaded(type, key))
            {
                return this.loadingCache.Retrieve(type, key);
            }

            // Raise the loading event
            if (this.Loading != null)
            {
                this.Loading(this, new KeyEventArgs(type, key));
            }

            using (IsolatedStorageFile store = StorageFactory.Create())
            {
                string path = this.DefineInstancePath(definition, key);
                if (store.FileExists(path))
                {
                    using (Stream stream = store.OpenFile(path, FileMode.Open))
                    {
                        // Initialize the instance
                        object instance = definition.CreateInstance(key);
                        this.loadingCache.Loaded(type, key, instance);

                        // Read and initialize its content
                        BinaryReader reader = new BinaryReader(stream);
                        ISerializer serializer = this.classSerializers[type];
                        serializer.Deserialize(reader, type, instance);

                        // Raise the loaded event
                        if (this.Loaded != null)
                        {
                            this.Loaded(this, new InstanceEventArgs(type, instance));
                        }

                        return instance;
                    }
                }
                else
                {
                    // Can't find the instance - can have been removed
                    return null;
                }
            }
        }

        /// <summary>
        /// Implements the saving logic.
        /// </summary>
        /// <param name="type">The type of the saved object.</param>
        /// <param name="value">The value of the saved object.</param>
        /// <seealso cref="DataSource.Save{T}(T)"/>
        internal void Save(Type type, object value)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }
            else if (value == null)
            {
                throw new ArgumentNullException("value");
            }
            else if (!this.classes.ContainsKey(type))
            {
                string message = string.Format(Messages.Culture, Messages.DataSourceUnknownClass, type.FullName);
                throw new UnknownClassException(message);
            }

            IClassDefinition definition = this.classes[type];
            object key = definition.RetrieveKey(value);
            if (this.savingCache.IsSaved(type, key))
            {
                return;
            }

            // Raise the saving event
            if (this.Saving != null)
            {
                this.Saving(this, new InstanceEventArgs(type, value));
            }

            this.savingCache.Saved(type, key);
            string path = this.DefineInstancePath(definition, key);
            using (IsolatedStorageFile store = StorageFactory.Create())
            {
                store.CreateDirectory(Path.GetDirectoryName(path));
                using (Stream stream = store.OpenFile(path, FileMode.Create))
                {
                    BinaryWriter writer = new BinaryWriter(stream);
                    ISerializer serializer = this.classSerializers[type];
                    serializer.Serialize(writer, type, value);
                }

                // Updates the indexes
                this.indexManagers[type].OnSave(value);

                // Raise the saved event
                if (this.Saved != null)
                {
                    this.Saved(this, new InstanceEventArgs(type, value));
                }
            }
        }

        /// <summary>
        /// Loads the indexes associated to the index managers.
        /// </summary>
        private void LoadIndexes()
        {
            IndexManagerSerializer serializer = new IndexManagerSerializer(this.propertySerializers);
            foreach (IndexManager manager in this.indexManagers.Values)
            {
                string path = this.DefineIndexPath(manager);
                using (IsolatedStorageFile store = StorageFactory.Create())
                {
                    if (!store.FileExists(path))
                    {
                        break;
                    }

                    using (Stream stream = store.OpenFile(path, FileMode.Open))
                    {
                        BinaryReader writer = new BinaryReader(stream);
                        serializer.Deserialize(writer, typeof(IndexManager), manager);
                    }
                }
            }
        }

        /// <summary>
        /// Stores the indexes associated to the index managers.
        /// </summary>
        private void SaveIndexes()
        {
            IndexManagerSerializer serializer = new IndexManagerSerializer(this.propertySerializers);
            foreach (IndexManager manager in this.indexManagers.Values)
            {
                string path = this.DefineIndexPath(manager);
                using (IsolatedStorageFile store = StorageFactory.Create())
                {
                    store.CreateDirectory(Path.GetDirectoryName(path));
                    using (Stream stream = store.OpenFile(path, FileMode.Create))
                    {
                        BinaryWriter writer = new BinaryWriter(stream);
                        serializer.Serialize(writer, typeof(IndexManager), manager);
                    }
                }
            }
        }

        /// <summary>
        /// Defines the path of an instance.
        /// </summary>
        /// <param name="definition">The class definition.</param>
        /// <param name="key">The key of the persisted instance.</param>
        /// <returns>The path used to store the instance.</returns>
        private string DefineInstancePath(IClassDefinition definition, object key)
        {
            return IsolatedStorageHelper.Combine(this.Name, definition.Type.Name, "data", key.ToString());
        }

        /// <summary>
        /// Defines the path of an index manager.
        /// </summary>
        /// <param name="manager">The reference index manager.</param>
        /// <returns>The path used to store the indexes.</returns>
        private string DefineIndexPath(IndexManager manager)
        {
            return IsolatedStorageHelper.Combine(this.Name, manager.ClassType.Name, "index");
        }
    }
}
